package com.googlecode.pdfToCMaker;

import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDJpeg;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectImage;
import org.apache.pdfbox.pdmodel.interactive.action.type.PDAction;
import org.apache.pdfbox.pdmodel.interactive.action.type.PDActionGoTo;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitWidthDestination;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

public class TableOfContentsMaker {

    // NOTE ON MAKING CHANGES TO THIS CLASS.
    // -------------------------------------
    // Every change made to what gets drawn in the PDF (in makeTOCPage()) should be taken account of in
    // getPageOfTocEntries(). The reason for this is that we need to know before hand how many pages the TOC
    // will be so that when creating the TOC, page numbers can be adjusted accordingly.

    static final PDFont TITLE_FONT = PDType1Font.HELVETICA_BOLD;
    static final int TITLE_FONT_SIZE = 18;
    static final PDFont HEADER_FONT = PDType1Font.HELVETICA_BOLD;
    static final int HEADER_FONT_SIZE = 14;
    static final PDFont ENTRY_FONT = PDType1Font.HELVETICA;
    static final int ENTRY_FONT_SIZE = 12;
    static final PDFont FOOTER_FONT = PDType1Font.HELVETICA_BOLD;
    static final int FOOTER_FONT_SIZE = 10;
    static final PDFont PAGE_NUMBER_FONT = PDType1Font.HELVETICA;
    static final int PAGE_NUMBER_FONT_SIZE = 10;

    static final int ENTRY_INDENT = 12;
    static final int LEFT_MARGIN = 80;
    static final int SPACER = 10;
    static final int PAGE_WIDTH = 450;
    static final int MAX_PAGE_HEIGHT = 750;
    static final int MAX_CHARS_PER_LINE = 60;

    private LogoInformation logoInformation;
    private String[] footer;
    private boolean addPageNumbers = false;

    public void combinePDFWithTableOfContents(List<TOCEntry> tocList, String title, String outputPath) {
        try {
            File outputFile = new File(outputPath);
            String directoryPath = outputFile.getParent();
            File toc = new File(directoryPath + File.separator + "toc_for_" + outputFile.getName());
            toc = generateTOC(tocList, title, toc.getAbsolutePath());
            List<File> filesList = new ArrayList<File>();
            filesList.add(toc);
            for (TOCEntry tocEntry : tocList) {
                if (tocEntry.getFile() != null) {
                    filesList.add(tocEntry.getFile());
                }
            }
            PDFUtil.combinePDF(filesList, outputPath);
            toc.delete();
            setLinks(outputFile, tocList);
            if (addPageNumbers) {
                addPageNumbersToFile(outputFile);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    File generateTOC(List<TOCEntry> tocEntries, String title, String outputPath) {
        PDDocument document = null;
        splitTOCEntriesThatAreTooLong(tocEntries);
        try {
            document = new PDDocument();
            PDXObjectImage logoImage = makeLogoImage(document);
            List<List<TOCEntry>> tocPages = breakTOCIntoPages(tocEntries);
            addPageNumbersToTOCEntries(tocEntries, tocPages.size());
            for (List<TOCEntry> tocPage : tocPages) {
                makeTOCPage(document, tocPage, title, logoImage);
            }
            document.save(outputPath);

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            closeDocument(document);
        }
        return new File(outputPath);
    }

    void splitTOCEntriesThatAreTooLong(List<TOCEntry> tocEntries) {
        for (TOCEntry tocEntry : tocEntries) {
            String titleWithLineBreaks = addLineBreaks(tocEntry.getTitle(), MAX_CHARS_PER_LINE);
            tocEntry.setTitleSplitIntoLines(Arrays.asList(titleWithLineBreaks.split("\n")));
        }
    }

    private void closeDocument(PDDocument document) {
        if (document != null) {
            try {
                document.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    static void addPageNumbersToTOCEntries(List<TOCEntry> tocEntries, int tocSize) {
        int currPage = tocSize;
        for (TOCEntry tocEntry : tocEntries) {
            if (tocEntry.isHeader()) {
                continue;
            }
            tocEntry.setPageNumber(currPage + 1);
            currPage += PDFUtil.getPageCount(tocEntry.getFile());
        }
    }

    private void makeTOCPage(PDDocument document, List<TOCEntry> currPage, String title, PDXObjectImage logoImage) throws IOException {
        PDPage page = new PDPage();
        document.addPage(page);
        PDPageContentStream contentStream = new PDPageContentStream(document, page);

        float pageHeight = page.getMediaBox().getHeight();
        float pageWidth = page.getMediaBox().getWidth();
        List<PDAnnotation> annotations = page.getAnnotations();

        // title
        makeTitleArea(title, logoImage, pageHeight, pageWidth, contentStream);

        float verticalDistance = pageHeight - getTitleAreaHeight();
        contentStream.beginText();
        contentStream.moveTextPositionByAmount(LEFT_MARGIN, 0);  //indent
        contentStream.moveTextPositionByAmount(0, verticalDistance);

        // entries
        for (int i = 0; i < currPage.size(); i++) {
            TOCEntry tocEntry = currPage.get(i);
            makeTOCLine(contentStream, annotations, tocEntry, verticalDistance);
            verticalDistance -=  tocEntry.getTitleSplitIntoLines().size() *
                    ((tocEntry.isHeader()) ? HEADER_FONT_SIZE : ENTRY_FONT_SIZE);
            // add a spacer if the next thing is a new header
            if ((i < currPage.size() - 1) && currPage.get(i + 1).isHeader()) {
                contentStream.moveTextPositionByAmount(0, -SPACER);
                verticalDistance -= SPACER;
            }
        }

        // footer
        makeFooter(contentStream, verticalDistance);

        contentStream.endText();
        contentStream.close();
    }

    private int getTitleAreaHeight() {
        return Math.max(getLogoHeight(), TITLE_FONT_SIZE) + (4 * SPACER);
    }

    private void makeTitleArea(String title, PDXObjectImage logoImage, float pageHeight, float pageWidth,
                                      PDPageContentStream contentStream) throws IOException {
        if (logoImage != null) {
            contentStream.drawImage(logoImage, pageWidth - getLogoWidth() - SPACER, pageHeight - getLogoHeight() - SPACER);
        }
        int logoTitleHeight = Math.max(getLogoHeight(), TITLE_FONT_SIZE);
        contentStream.beginText();
        contentStream.moveTextPositionByAmount(LEFT_MARGIN, pageHeight - logoTitleHeight);
        contentStream.setFont(TITLE_FONT, TITLE_FONT_SIZE);
        contentStream.drawString(title);
        contentStream.endText();
        float lineY = pageHeight - logoTitleHeight - 2 * SPACER;
        contentStream.drawLine(SPACER, lineY, pageWidth - SPACER, lineY);
    }

    private int getLogoHeight() {
        return logoInformation == null ? 0 : logoInformation.getLogoHeight();
    }
    private float getLogoWidth() {
        return logoInformation == null ? 0 : logoInformation.getLogoWidth();
    }

    private int getFooterHeight() {
        return  footer == null ? 0 : footer.length * FOOTER_FONT_SIZE;
    }

    private void makeFooter(PDPageContentStream contentStream, float verticalDistance) throws IOException {
        if (footer == null) {
            return;
        }
        int footerHeight = SPACER + getFooterHeight();

        contentStream.moveTextPositionByAmount(-(.5F * LEFT_MARGIN), 0); // move a little to the right so it fits on a page.
        contentStream.moveTextPositionByAmount(0, -(verticalDistance - footerHeight));

        contentStream.setFont(FOOTER_FONT, FOOTER_FONT_SIZE);
        for (int i=0; i<footer.length; i++) {
            contentStream.drawString(footer[i]);
            if (i < footer.length - 1) {
                contentStream.moveTextPositionByAmount(0, -FOOTER_FONT_SIZE);
            }
        }
    }

    private void makeTOCLine(PDPageContentStream contentStream, List<PDAnnotation> annotations,
                                    TOCEntry tocEntry, float verticalDistance) throws IOException {
        if (tocEntry.isHeader()) {
            drawHeaderLine(contentStream, tocEntry);
        } else {
            drawEntryLine(contentStream, annotations, tocEntry, verticalDistance);
        }
    }

    private void drawHeaderLine(PDPageContentStream contentStream, TOCEntry tocEntry) throws IOException {
        contentStream.setFont(HEADER_FONT, HEADER_FONT_SIZE);
        for (String line : tocEntry.getTitleSplitIntoLines()) {
            contentStream.drawString(line);
            contentStream.moveTextPositionByAmount(0, -HEADER_FONT_SIZE);
        }
    }

    private void drawEntryLine(PDPageContentStream contentStream, List<PDAnnotation> annotations,
                                      TOCEntry tocEntry, float verticalDistance) throws IOException {
        String pageNumberString = Integer.toString(tocEntry.getPageNumber());
        float numberWidth = (ENTRY_FONT.getStringWidth(pageNumberString) / 1000) * ENTRY_FONT_SIZE;
        float dotWidth = (ENTRY_FONT.getStringWidth(".") / 1000) * ENTRY_FONT_SIZE;

        contentStream.setFont(ENTRY_FONT, ENTRY_FONT_SIZE);

        for (int i = 0; i < tocEntry.getTitleSplitIntoLines().size(); i++) {
            float amountShiftedRight = 0;

            // Indent
            int indentAmount = (i==0) ? ENTRY_INDENT : ENTRY_INDENT * 2;
            contentStream.moveTextPositionByAmount(indentAmount, 0);
            amountShiftedRight += indentAmount;

            // Text
            String line = tocEntry.getTitleSplitIntoLines().get(i);
            contentStream.drawString(line);
            float textWidth = (ENTRY_FONT.getStringWidth(line) / 1000) * ENTRY_FONT_SIZE;
            contentStream.moveTextPositionByAmount(textWidth, 0);
            amountShiftedRight += textWidth;

            // Link on entry title
            PDAnnotationLink link = new PDAnnotationLink();
            PDActionGoTo action = new PDActionGoTo();
            action.setDestination(new PDNamedDestination("" + (tocEntry.getPageNumber() - 1)));
            link.setAction(action);
            link.setHidden(true);
            annotations.add(link);

            PDRectangle pdRectangle = new PDRectangle();
            pdRectangle.setLowerLeftX(LEFT_MARGIN + amountShiftedRight - textWidth);
            pdRectangle.setLowerLeftY(verticalDistance - ((i-1) * ENTRY_FONT_SIZE));
            pdRectangle.setUpperRightX(LEFT_MARGIN + amountShiftedRight);
            pdRectangle.setUpperRightY(verticalDistance - (i * ENTRY_FONT_SIZE));
            link.setRectangle(pdRectangle);


            if (i == tocEntry.getTitleSplitIntoLines().size() -1 ) { //if last one
                // add dots
                float availableSpace = PAGE_WIDTH - numberWidth - amountShiftedRight;
                int numDots = (int) (availableSpace / dotWidth);
                for (int j = 0; j < numDots; j++) {
                    contentStream.drawString(".");
                    contentStream.moveTextPositionByAmount(dotWidth, 0);
                    amountShiftedRight += dotWidth;
                }

                // add page number
                contentStream.drawString(pageNumberString);
            }

            // Reset
            contentStream.moveTextPositionByAmount(-amountShiftedRight, 0);
            contentStream.moveTextPositionByAmount(0, -ENTRY_FONT_SIZE);
        }
    }

    List<List<TOCEntry>> breakTOCIntoPages(List<TOCEntry> tocEntries) {
        List<List<TOCEntry>> ret = new ArrayList<List<TOCEntry>>();
        int tocPageStart = 0;
        while (tocPageStart < tocEntries.size()) {
            List<TOCEntry> currPage = getPageOfTocEntries(tocEntries, tocPageStart);
            tocPageStart += currPage.size();
            ret.add(currPage);
        }
        return ret;
    }

    private List<TOCEntry> getPageOfTocEntries(List<TOCEntry> tocEntries, int tocPageStart) {
        List<TOCEntry> ret = new ArrayList<TOCEntry>();
        int currHeight = getTitleAreaHeight() + SPACER + getFooterHeight();
        for (int i = tocPageStart; i < tocEntries.size(); i++) {
            TOCEntry currEntry = tocEntries.get(i);
            int currEntryHeight = (currEntry.isHeader()) ? HEADER_FONT_SIZE : ENTRY_FONT_SIZE;
            currEntryHeight *= currEntry.getTitleSplitIntoLines().size();
            boolean doesEntryFit = currEntryHeight < (MAX_PAGE_HEIGHT - currHeight);
            if (!doesEntryFit) {
                break;
            }
            ret.add(currEntry);
            currHeight += currEntryHeight;
            if ((i < tocEntries.size() - 1) && tocEntries.get(i + 1).isHeader()) {
                currHeight += SPACER;
            }
        }
        return ret;
    }

    public void setLinks(File file, List<TOCEntry> tocList) throws IOException {
        List<List<TOCEntry>> tocPageList = breakTOCIntoPages(tocList);

        PDDocument document = PDDocument.load(file);
        List allPages = document.getDocumentCatalog().getAllPages();
        for (int i = 0; i < tocPageList.size(); i++) {
            PDPage page = (PDPage) allPages.get(i);
            List<PDAnnotation> annotations = page.getAnnotations();
            for (PDAnnotation annotation : annotations) {
                if (annotation instanceof PDAnnotationLink) {
                    PDAnnotationLink annotationLink = (PDAnnotationLink) annotation;
                    PDAction action = annotationLink.getAction();
                    if (action instanceof PDActionGoTo) {
                        PDActionGoTo actionGoTo = (PDActionGoTo) action;
                        PDDestination destination = actionGoTo.getDestination();
                        if (destination instanceof PDNamedDestination) {
                            PDNamedDestination namedDestination = (PDNamedDestination) destination;
                            String destinationString = namedDestination.getNamedDestination();
                            int destinationPageInt = Integer.parseInt(destinationString);
                            PDPageFitWidthDestination pageDestination = new PDPageFitWidthDestination();
                            pageDestination.setPage((PDPage) (allPages.get(destinationPageInt)));
                            actionGoTo.setDestination(pageDestination);
                        }
                    }
                }
            }
        }
        try {
            document.save(file.getAbsolutePath());
        } catch (COSVisitorException e) {
            throw new RuntimeException(e);
        }
    }

    private void addPageNumbersToFile(File file) throws IOException {
        PDDocument document = PDDocument.load(file);
        List allPages = document.getDocumentCatalog().getAllPages();
        for (int i=0; i<allPages.size(); i++) {
            PDPage page = (PDPage) allPages.get(i);
            PDPageContentStream contentStream = new PDPageContentStream(document, page, true, true, true);
            contentStream.beginText();
            contentStream.moveTextPositionByAmount(page.getMediaBox().getWidth() / 2,  SPACER * 2);
            contentStream.setFont(PAGE_NUMBER_FONT, PAGE_NUMBER_FONT_SIZE);
            contentStream.drawString(Integer.toString(i+1));
            contentStream.endText();
            contentStream.close();
        }
        try {
            document.save(file.getAbsolutePath());
        } catch (COSVisitorException e) {
            throw new RuntimeException(e);
        }
    }

    private PDXObjectImage makeLogoImage(PDDocument document) throws IOException {
        if (logoInformation == null) {
            return null;
        }
        InputStream logoStream = TableOfContentsMaker.class.getResourceAsStream(logoInformation.getLogoPath());
        BufferedImage bufferedImage = ImageIO.read(logoStream);
        return new PDJpeg(document, bufferedImage);
    }

    public String addLineBreaks(String input, int maxLineLength) {
        StringTokenizer tok = new StringTokenizer(input, " ");
        StringBuilder output = new StringBuilder();
        int lineLen = 0;
        while (tok.hasMoreTokens()) {
            String word = tok.nextToken();

            if (lineLen + word.length() > maxLineLength) {
                output.append("\n");
                lineLen = 0;
            }
            output.append(word).append(" ");
            lineLen += word.length() + 1; // +1 for space
        }
        return output.toString();
    }

    public void setLogoInformation(LogoInformation logoInformation) {
        this.logoInformation = logoInformation;
    }

    public void setFooter(String[] footer) {
        this.footer = footer;
    }

    public void setAddPageNumbers() {
        this.addPageNumbers = true;
    }
}
